Skip to content

[4.x] Add token-based security for cart loading#4207

Open
lukeholder wants to merge 12 commits into
4.xfrom
feature/load-cart-token-4x
Open

[4.x] Add token-based security for cart loading#4207
lukeholder wants to merge 12 commits into
4.xfrom
feature/load-cart-token-4x

Conversation

@lukeholder
Copy link
Copy Markdown
Member

@lukeholder lukeholder commented Jan 21, 2026

Summary

Adds token-based security to the cart loading action, with a cart recovery email flow for users whose token has expired or is missing.

Changes

Security

  • Cart load URLs are now generated using Craft's built-in token system, creating time-limited, cryptographically-bound one-use URLs.
  • commerce/cart/load-cart now validates a token query parameter. Without a valid token, the user must be logged in as the cart's owner to proceed.
  • Carts with no email address and no billing/shipping addresses bypass token validation (anonymous/empty carts load freely, as before).

Cart Recovery Flow

  • Users with an invalid or missing token are redirected to an email challenge form (commerce/cart/email-challenge).
  • Submitting the form sends a fresh, time-limited recovery link to the cart's email address.
  • Added a new commerce_cart_recovery system message (customizable subject/body via Settings > Emails).
  • New CP-rendered templates: _cart/email-challenge.twig and _cart/email-sent.twig.
  • commerce/cart/load-cart returns JSON with a challengeUrl key for Accept: application/json requests on failure.

Settings

  • Added cartLoadUrlExpiry setting (int, seconds; default: 604800 / 7 days) to control how long cart load links remain valid.

CP "Share Cart" Element Action

  • The action now fetches a fresh tokenized URL via an AJAX request to commerce/orders/get-load-cart-url rather than constructing a static URL client-side, so every copied URL has a valid token.

New / Changed APIs

  • craft\commerce\services\Carts::getLoadCartUrl(Order $cart): string — creates a Craft token and returns the full load-cart URL.
  • craft\commerce\elements\Order::getLoadCartUrl() — now delegates to Carts::getLoadCartUrl() and returns a tokenized URL.
  • craft\commerce\controllers\CartController::actionEmailChallenge() — renders the cart recovery email challenge form.
  • craft\commerce\controllers\CartController::actionCartChallenge() — handles form submission and sends the recovery email.
  • craft\commerce\controllers\CartController::actionCartSent() — renders the post-send confirmation page.
  • craft\commerce\controllers\OrdersController::actionGetLoadCartUrl() — JSON endpoint (requires commerce-manageOrders) used by the CP "Share cart" element action.

- Add secure token validation to load-cart action
  - Carts with email/addresses require valid token or owner authentication
  - Carts without sensitive data can load without token
  - Add email challenge flow for unauthenticated cart recovery
  - Register commerce_cart_recovery system message for recovery emails
  - Add cartLinkExpiry setting (default 24 hours)
  - Add getLoadCartUrl() to Carts service for generating secure URLs
@lukeholder lukeholder changed the title Add token-based security for cart loading [4.x] Add token-based security for cart loading Jan 21, 2026
@rlarabee
Copy link
Copy Markdown

rlarabee commented Jan 25, 2026

Take a look at _getCart() in CartController.php as well because it is called by actionUpdateCart() and actionComplete() and any other cart modification actions, make sure the same validation is applied. I am not sure how this will affect the over all functionality, but from a cryptographic standpoint, for the generateCartNumber, I would move to something like bin2hex(random_bytes(16)).

@lukeholder lukeholder mentioned this pull request May 20, 2026
7 tasks
@lukeholder lukeholder marked this pull request as ready for review May 20, 2026 09:14
@lukeholder lukeholder requested a review from a team as a code owner May 20, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants